﻿
using System.Text;
using System.Collections.Generic;
using System;
using Pretext.Logging;
using Pretext.Formatters;
namespace Pretext {
    /// <summary>
    /// Class to parse Tokens and to generate the output strings.
    /// </summary>
    public class TokenParser {

        public TokenParser() {
            Tokens = new List<Token>();
            //Output = new StringBuilder();

            OutputCollection = new Dictionary<string, StringBuilder>();
            OutputPtr = "root";

            OutputCollection[OutputPtr] = new StringBuilder();
            Output = OutputCollection[OutputPtr];

            // create a new session
            this.Session = new PretextSession();

        }

        public TokenParser(PretextSession session)
            : this() {

            this.Session = session;
			
			// sets all constant variables
			this.Session.SetCommonVariables();

        }

        public List<Token> Tokens { get; private set; }
        public StringBuilder Output { get; private set; }
        public PretextSession Session { get; private set; }
        public Dictionary<string, string> Variables { get; private set; }
        public Dictionary<string, StringBuilder> OutputCollection { get; private set; }
        public string OutputPtr { get; private set; }

        //public List<string> Log { get; private set; }

        // state of the parser
        bool InsidePretext = false;
        bool InsideComment = false;

        public void Process() {

            while (HasNextToken) {
                switch (CurrentToken.Type) {
                    case TokenType.NormalText:
                        // write out the normal text
                        Output.Append(CurrentToken.Value);
                        break;
                    case TokenType.StartOfPretextBracket:
                        // switch the context
                        InsidePretext = true;
                        break;
                    case TokenType.EndOfPretextBracket:
                        // switch the context back
                        InsidePretext = false;
                        break;

                    case TokenType.StartComment:
                        //Start of a comment block
                        if (!InsidePretext && !InsideComment) InsideComment = false;
                        SeekNextToken(TokenType.EndComment);
                        break;

                    case TokenType.EndComment:
                        //end of a comment block
                        if (InsideComment) InsideComment = false;
                        break;
                    case TokenType.LineComment:
                        if (InsidePretext && !InsideComment) InsideComment = true;
                        break;
					case TokenType.AssignmentOperator:
                    case TokenType.Symbol:
                        if (InsidePretext && !InsideComment) {
                            // process the symbol
                            ProcessSymbolAndAssignmentOperators(CurrentToken);
                        }

                        if (InsidePretext && InsideComment && (
                            (CurrentToken.Value.ToString() + Tokens[CurrentPosition + 1].Value.ToString()) == "\n" ||
                            (CurrentToken.Value.ToString() + Tokens[CurrentPosition + 1].Value.ToString()) == "\r"))
                            InsideComment = false;

                        break;
                }

                // next one
                CurrentPosition++;

            }

        }

        void ProcessSymbolAndAssignmentOperators(Token symbol) {

            LogInfo("Processing symbol: \"" + symbol.Value + "\"");

            List<Token> args = null;
            string variableName = null;

            switch (symbol.Value.ToLower()) {
                case "=": // assignment or write shortcut

                    // is this the write shortcut?
                    if (!IsFirstToken && PreviousToken.Type == TokenType.StartOfPretextBracket) {

                        // remove the "=" symbol
                        Tokens.RemoveAt(CurrentPosition);

                        int startingPosition = CurrentPosition;

                        // insert the 'write' symbol
                        Tokens.Insert(CurrentPosition++, new Token(TokenType.Symbol, "write"));
                        Tokens.Insert(CurrentPosition++, new Token(TokenType.OpeningArgumentBracket, "("));
                        SeekNextToken(TokenType.EndOfPretextBracket);
                        Tokens.Insert(CurrentPosition, new Token(TokenType.ClosingArgumentBracket, ")"));

                        // put the current position back
                        CurrentPosition = startingPosition;

                        // now process this token
                        ProcessSymbolAndAssignmentOperators(CurrentToken);

                    } else {

                        // assignment operator

                        variableName = PreviousToken.Value;

                        // find the value
                        SeekNextToken(new TokenType[]{ 
                            TokenType.String, 
                            TokenType.Symbol, 
                            TokenType.BooleanValue
                        });

                        string variableValue = (CurrentToken.Type.Equals(TokenType.Symbol)) ? this.Session.Variables[CurrentToken.Value].ToString() : CurrentToken.Value;

                        // set the variable
                        this.Session.Variables[variableName] = variableValue;

                    }

                    break;

                case "+":
                    int i = PreviousTokenPosition("=");
                    Token rootToken = Tokens[i-1];
                    variableName = rootToken.Value;

                    // find the value
                    SeekNextToken(new TokenType[]{ 
                            TokenType.String, 
                            TokenType.Symbol, 
                            TokenType.BooleanValue
                        });

                    
                    if (CurrentToken.Type.Equals(TokenType.Symbol))
                    {
                        this.Session.Variables[variableName] = this.Session.Variables[variableName].ToString() + this.Session.Variables[CurrentToken.Value].ToString();
                    }
                    else {
                        this.Session.Variables[variableName] = this.Session.Variables[variableName].ToString() + CurrentToken.Value;
                    }
  
                    break;

                case "write":

                    args = GetArguments();

                    // validate it
                    if (args.Count != 1)
                        LogError("write method expects one argument.");

                    // write the argument
                    Output.Append(ProcessArgumentOrSymbol(args[0]));

                    break;
                case "include": // include another file

                    args = GetArguments();

                    // validate it
                    if (args.Count <= 0) LogError("include method expects at least one argument.");
                    
                    string source = ProcessArgumentOrSymbol(args[0]);
                    bool process = true;
                    bool as_c_string = false;
					bool remove_whitespace = false;
                    string output;
					string oldWorkingDirectory = Pretexter.WorkingDirectory;

                    if (ContainsFlag(args, ":verbatim"))
                        process = false; 
                    
                    if (ContainsFlag(args, ":as_c_string"))
                        as_c_string = true;

					if (ContainsFlag(args, ":no_whitespace"))
						remove_whitespace = true;

                    if (process)
                        output = Pretexter.LoadAndProcessFile(source, CurrentToken);
                    else
						output = Pretexter.LoadFile(source, CurrentToken);

					if (as_c_string) {
						CStringFormatter formatter = new CStringFormatter();
						output = formatter.Format(output);
					}

					if (remove_whitespace) {
						WhitespaceRemoverFormatter formatter = new WhitespaceRemoverFormatter();
						output = formatter.Format(output);
					}

                    Output.Append(output);

					Pretexter.WorkingDirectory = oldWorkingDirectory;

                    break;
                case "if": // conditional code

                    // get the "if" arguments
                    args = GetArguments();

                    // is the condition matched?
                    if (!EvaluateCondition(args)) {

                        // jump to the closing block

                        JumpToStartOfNextBlock();
                        JumpPastNextBlock();

                    }

                    break;
                case "output_to":
                    args = GetArguments();

                    if (args.Count != 1) LogError("output_to method expects one argument.");
                    string target = ProcessArgumentOrSymbol(args[0]);
                    this.setOutputPtr(target);
                    break;
            };

        }
        
        /// <summary>
        /// Checks for specified flags within arguments
        /// </summary>
        /// <returns></returns>
        public static bool ContainsFlag(List<Token> args, string flag) {

            foreach (Token arg in args) {

                if (arg.Value == flag)
                    return true;

            }

            return false;

        }

        /// <summary>
        /// Gets the value from the specified token.
        /// If the token is a string the value is returned, if the token is a variable its value is looked up in the Varaibles table.
        /// </summary>
        /// <param name="t">The token to get the value for.</param>
        /// <returns></returns>
        public string ProcessArgumentOrSymbol(Token t) {

            switch (t.Type) {
                case TokenType.String:
                    return t.Value;
                case TokenType.Symbol:
                    if (this.Session.Variables.ContainsKey(t.Value))
                        return this.Session.Variables[t.Value];
                    else
                        return String.Empty;
            }

            LogError("Nothing could be understood by '" + t.Value + "' .");
            return null;
			
        }

        /// <summary>
        /// Jumps to the start of the next block
        /// </summary>
        public void JumpToStartOfNextBlock() {

            SeekNextToken(TokenType.OpeningBlockBracket);

        }

        /// <summary>
        /// Jumps to the first token after the next closing brace.
        /// </summary>
        public void JumpPastNextBlock() {

            int currentLevel = CurrentToken.Level;

            CurrentPosition++;

            while (CurrentToken.Level > currentLevel) {
                CurrentPosition++;
            }

        }

        void LogInfo(string message) {
			this.Session.AddLogItem(new LogItem(LogItemType.Info, message, CurrentToken));
        }
        
		void LogWarn(string message) {
			this.Session.AddLogItem(new LogItem(LogItemType.Warning, message, CurrentToken));
        }
        
		void LogError(string message) {
			this.Session.AddLogItem(new LogItem(LogItemType.Error, message, CurrentToken));
        }

        /// <summary>
        /// Gets or sets the current position
        /// </summary>
        public int CurrentPosition { get; protected set; }

        /// <summary>
        /// Gets whether the current token is the first token or not.
        /// </summary>
        protected bool IsFirstToken {
            get {
                return CurrentPosition == 0;
            }
        }
        /// <summary>
        /// Gets whether the current token is the last token or not.
        /// </summary>
        protected bool IsLastToken {
            get {
                return CurrentPosition == (Tokens.Count - 1);
            }
        }
        /// <summary>
        /// Gets whether there is another token in the queue or not.
        /// </summary>
        protected bool HasNextToken {
            get {
                return CurrentPosition < (Tokens.Count);
            }
        }
        /// <summary>
        /// Gets the previous token or returns null if there isn't one.
        /// </summary>
        protected Token PreviousToken {
            get {
                if (!IsFirstToken) return Tokens[CurrentPosition - 1];
                return null;
            }
        }

        /// <summary>
        /// Gets the next token or returns null if there isn't one.
        /// </summary>
        protected Token NextToken {
            get {
                if (IsLastToken) return null;
                return Tokens[CurrentPosition + 1];
            }
        }

        /// <summary>
        /// Gets the current token
        /// </summary>
        protected Token CurrentToken {
            get {
                return Tokens[CurrentPosition];
            }
        }

        /// <summary>
        /// Seeks the next token.
        /// </summary>
        /// <returns>Returns a boolean indicating whether another token was found or not.</returns>
        protected bool SeekNextToken() {
            if (CurrentPosition == (Tokens.Count - 1)) return false;
            CurrentPosition++;
            return true;
        }

        /// <summary>
        /// Seeks the next token of a specific type.
        /// </summary>
        /// <param name="type">The type of token to find.</param>
        /// <returns>Returns a boolean indicating whether another token of the specified type was found or not.</returns>
        protected bool SeekNextToken(TokenType type) {

            while ((CurrentPosition + 1) < Tokens.Count) {
                CurrentPosition++;
                if (Tokens[CurrentPosition].Is(type))
                    break;
            }

            return Tokens[CurrentPosition].Is(type);

        }

        /// <summary>
        /// Seeks the next token whose type is any of the specified types.
        /// </summary>
        /// <param name="types">The types of token to find.</param>
        /// <returns>Returns a boolean indicating whether another token of one of the specified types was found or not.</returns>
        protected bool SeekNextToken(TokenType[] types) {

            bool found = false;

            while ((CurrentPosition + 1) < Tokens.Count) {
                CurrentPosition++;

                foreach (TokenType t in types)
                    if (Tokens[CurrentPosition].Is(t)) {
                        found = true;
                        break;
                    }
                if (found) break;
            }
            return found;
        }

        //fetch previous token, but don't change pointer
        protected Token PeekPreviousToken(string s) {
            Token peekabo = new Token();
            bool found = false;
            int peekPosition = CurrentPosition;
            while (peekPosition != -1) {
                peekPosition--;
                if (Tokens[peekPosition].Value.ToString() == s) {
                    found = true;
                    peekabo = Tokens[peekPosition];
                    break;
                }
            }
            return (found) ? peekabo : null;
        }

        //fetch first token before marker, but don't change pointer
        protected Token PeekPreviousToken(string s, Token marker) {
            Token peekabo = new Token();
            Token init = new Token();
            bool found = false;
            int peekPosition = CurrentPosition;
            while (peekPosition != -1) {
                peekPosition--;
                if (Tokens[peekPosition].Equals(marker)) {
                    found = true;
                    init = Tokens[peekPosition];
                    break;
                }
            }

            return (found) ? peekabo : null;
        }

        protected Token PeekPreviousToken(TokenType type) {
            Token peekabo = new Token();
            bool found = false;
            int peekPosition = CurrentPosition;
            while (peekPosition != -1) {
                peekPosition--;
                if (Tokens[peekPosition].Type.Equals(type)) {
                    found = true;
                    peekabo = Tokens[peekPosition];
                    break;
                }


            }

            return (found) ? peekabo : null;
        }

        protected int PreviousTokenPosition(string s) {
            Token peekabo = new Token();
            bool found = false;
            int peekPosition = CurrentPosition;
            while (peekPosition != -1) {
                peekPosition--;
                if (Tokens[peekPosition].Value.ToString() == s) {
                    found = true;
                    break;
                }
            }

            return (found) ? peekPosition : 0;
        }

        protected int PreviousTokenPosition(TokenType type) {
            Token peekabo = new Token();
            bool found = false;
            int peekPosition = CurrentPosition;
            while (peekPosition != -1) {
                peekPosition--;
                if (Tokens[peekPosition].Type.Equals(type)) {
                    found = true;
                    break;
                }
            }

            return (found) ? peekPosition : 0;
        }


        //fetch next token, but don't change pointer
        protected Token PeekNextToken(string s) {
            Token peekabo = new Token();
            bool found = false;
            int peekPosition = CurrentPosition;
            while ((peekPosition + 1) < Tokens.Count) {
                peekPosition++;
                if (Tokens[peekPosition].Value.ToString() == s) {
                    found = true;
                    peekabo = Tokens[peekPosition];
                    break;
                }
            }

            return (found) ? peekabo : null;
        }

        protected Token PeekNextToken(TokenType type) {
            Token peekabo = new Token();
            bool found = false;
            int peekPosition = CurrentPosition;
            while ((peekPosition + 1) < Tokens.Count) {
                peekPosition++;

                if (Tokens[peekPosition].Type.Equals(type)) {
                    found = true;
                    peekabo = Tokens[peekPosition];
                    break;
                }
            }
            return (found) ? peekabo : null;
        }

        protected int NextTokenPosition(string s) {
            Token peekabo = new Token();
            bool found = false;
            int peekPosition = CurrentPosition;
            while ((peekPosition + 1) < Tokens.Count) {
                peekPosition++;
                if (Tokens[peekPosition].Value.ToString() == s) {
                    found = true;
                    break;
                }
            }

            return (found) ? peekPosition : 0;
        }

        protected int NextTokenPosition(TokenType type) {
            Token peekabo = new Token();
            bool found = false;
            int peekPosition = CurrentPosition;
            while ((peekPosition + 1) < Tokens.Count) {
                peekPosition++;
                if (Tokens[peekPosition].Type.Equals(type)) {
                    found = true;
                    break;
                }
            }

            return (found) ? peekPosition : 0;
        }


        public List<Token> GetArguments() {

            // move to the next start of arguments
            SeekNextToken(TokenType.OpeningArgumentBracket);

#if DEBUG
            if (!Tokens[CurrentPosition].Is(TokenType.OpeningArgumentBracket))
                throw new SyntaxException("Arguments are expected but missing.");
#endif

            List<Token> args = new List<Token>();

            // keep collecting arguments until we reach the argument closing bracket
            while (Tokens[CurrentPosition].Type != TokenType.ClosingArgumentBracket) {

                switch (Tokens[CurrentPosition].Type) {
                    case TokenType.String:
                    case TokenType.EqualityOperator:
                    case TokenType.InequalityOperator:
                    case TokenType.Symbol:

                        args.Add(Tokens[CurrentPosition]);

                        break;
                }

                // next token
                CurrentPosition++;
            }

            LogInfo("Arguments:");
            foreach (Token arg in args)
                LogInfo("   '" + arg.Value + "' (" + arg.Type.ToString() + ")");

            return args;

        }

        void CheckForAndParseWriteShortcutMethod() {

            /*
            *  Check for the write shortcut method
            */
            if (Tokens[CurrentPosition].Is(TokenType.Symbol, "="))
                if (!IsFirstToken)
                    if (PreviousToken.Is(TokenType.StartOfPretextBracket)) {

                        // remove the "=" symbol
                        Tokens.RemoveAt(CurrentPosition);

                        // insert the 'write' symbol
                        Tokens.Insert(CurrentPosition++, new Token(TokenType.Symbol, "write"));
                        Tokens.Insert(CurrentPosition++, new Token(TokenType.GeneralSyntaxGrammer, "("));

                        SeekNextToken(TokenType.EndOfPretextBracket);

                        Tokens.Insert(CurrentPosition, new Token(TokenType.GeneralSyntaxGrammer, ")"));

                    }

        }

        public bool EvaluateCondition(List<Token> conditionArguments) {

            int position = 0;

            bool result = true;

            Token currentToken = null;
            Token nextToken = null;

            while (true) {

                currentToken = conditionArguments[position];
                bool hasNextToken = position < (conditionArguments.Count - 1);

                if (hasNextToken)
                    nextToken = conditionArguments[position + 1];
                else
                    nextToken = null;

                // is this a symbol?
                if (currentToken.Type == TokenType.Symbol) {

                    // SYMBOL

                    if (hasNextToken && Pretexter.IsOperator(nextToken.Type)) {

                        // SYMBOL OPERATOR

                        Token test = conditionArguments[position + 2];

                        switch (nextToken.Type) {
                            case TokenType.EqualityOperator:

                                if (ProcessArgumentOrSymbol(currentToken) != ProcessArgumentOrSymbol(test)) {
                                    result = false;
                                }

                                break;
                            case TokenType.InequalityOperator:

                                if (ProcessArgumentOrSymbol(currentToken) == ProcessArgumentOrSymbol(test)) {
                                    result = false;
                                }

                                break;
                            default:
                                throw new ArgumentOutOfRangeException("OperatorToken.Type");
                        }

                        position += 2;

                    }
                    else {

                        if (!this.Session.Variables.ContainsKey(currentToken.Value) || string.IsNullOrEmpty(this.Session.Variables[currentToken.Value]))
                            result = false;

                    }
                }

                // next position
                position++;

                // if we have reached the end - exit the loop
                if (position == conditionArguments.Count)
                    break;

            }
            return result;
        }

        //the user doesn't need to know that the first Output is root
        public void resetOutput() {
            this.OutputPtr = "root";
            this.Output = this.OutputCollection[this.OutputPtr];
            return;
        }

        //get a different output
        public void setOutputPtr(string ptr) {
            this.OutputPtr = ptr;

            if (!this.OutputCollection.ContainsKey(ptr))
                this.OutputCollection.Add(ptr, new StringBuilder());

            this.Output = this.OutputCollection[this.OutputPtr];
            return;
        }

        //get the [file]name of the output
        public string getOutputPtr() {
            return this.OutputPtr;
        }

        public int countOutputs() {
            return this.OutputCollection.Keys.Count;
        }

    }
}
